/*   
 * Copyright 2009-2010 Rob Schoening
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 *    
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.fortasoft.hatfield.util.xml;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.fortasoft.hatfield.util.AnonymousFunction;
import org.fortasoft.hatfield.util.exception.HatfieldException;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

public class DomUtil {

    public static CDATASection appendCDATA(Node parent, String val) {
        CDATASection s = parent.getOwnerDocument().createCDATASection(val);
        parent.appendChild(s);
        return s;
    }


    public static Element appendChildElement(Node parent, String ns, String name) {
        Document d;
        if (parent instanceof Document) {
            d = (Document) parent;
        } else {
            d = parent.getOwnerDocument();
        }
        Element tmp = d.createElementNS(ns, name);
        parent.appendChild(tmp);
        return tmp;
    }


    public static Element appendChildElement(Node parent, String name) {
        Document d;
        if (parent instanceof Document) {
            d = (Document) parent;
        } else {
            d = parent.getOwnerDocument();
        }
        Element tmp = d.createElement(name);
        parent.appendChild(tmp);
        return tmp;

    }


    public static Element appendChildElementNS(Node parent, String ns,
            String qualifiedName) {
        Document document = parent.getOwnerDocument();
        Element element = document.createElementNS(ns, qualifiedName);
        parent.appendChild(element);
        return element;
    }


    public static Text appendTextNode(Node parent, String val) {
        Text tmp = parent.getOwnerDocument().createTextNode(val);
        parent.appendChild(tmp);
        return tmp;
    }


    public static List<Node> toList(NodeList nl) {
        ArrayList<Node> list = new ArrayList<Node>(nl.getLength());
        for (int i = 0; i < nl.getLength(); i++) {
            list.add(nl.item(i));
        }
        return list;
    }


    public static Iterator<Node> toIterator(final NodeList nl) {

        return new Iterator<Node>() {
            int index = -1;


            public boolean hasNext() {
                // TODO Auto-generated method stub
                return index + 1 < nl.getLength();
            }


            public Node next() {
                return nl.item(++index);
            }


            public void remove() {
                throw new UnsupportedOperationException();

            }

        };
    }


    public static void logError(org.slf4j.Logger log, String msg, Node n) {

        if (log.isErrorEnabled()) {
            log.error(logFormat(msg, n));
        }

    }


    public static void logWarn(org.slf4j.Logger log, String msg, Node n) {

        if (log.isWarnEnabled()) {
            log.warn(logFormat(msg, n));
        }

    }


    public static void logInfo(org.slf4j.Logger log, String msg, Node n) {

        if (log.isInfoEnabled()) {
            log.info(logFormat(msg, n));
        }

    }


    public static void logTrace(org.slf4j.Logger log, String msg, Node n) {

        if (log.isTraceEnabled()) {
            log.trace(logFormat(msg, n));
        }

    }


    private static String logFormat(String msg, Node n) {
        StringWriter sw = new StringWriter();
        sw.append("" + msg);
        sw.append(" (xml follows...)");
        sw.append(System.getProperty("line.separator", "\n"));
        if (n != null) {
            DomUtil.prettyPrint(sw, n);
        }
        return (sw.toString());
    }


    public static void logDebug(org.slf4j.Logger log, String msg, Node n) {

        if (log.isDebugEnabled()) {
            log.debug(logFormat(msg, n));
        }

    }


    public static void recurse(Node n, AnonymousFunction<Object, Node> na) {
        NodeList nl = n.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            recurse(nl.item(i), na);
        }
        na.fn(n);
    }


    public static void remove(Node n, String xpath) {
        NodeList nl = new XPathUtil().evalNodeList(n, xpath);
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);

            Node parent = node.getParentNode();
            if (parent != null) {
                parent.removeChild(node);
            }
        }
    }


    public static void removeWhitespaceNodes(Node e) {
        remove(e, "//text()[normalize-space(.) = '']");
    }


    public static void trimWhitespaceFromTextNodes(Node n) {

        AnonymousFunction<Object, Node> fn = new AnonymousFunction<Object, Node>() {
            public Object fn(Node n) {

                if (n.getNodeType() == Node.TEXT_NODE) {
                    String content = n.getTextContent();
                    if (content != null) {
                        n.setTextContent(content.trim());
                    }
                }
                return null;
            }
        };
        recurse(n, fn);

    }


    public static void prettyPrint(Node n) {
        prettyPrint(System.out, n);
    }


    public static void prettyPrint(Writer out, Node n) {
        write(out, n, true);
    }


    public static void prettyPrint(OutputStream out, Node n) {
        write(out, n, true);
    }


    public static void write(OutputStream out, Node n, boolean prettyPrint) {
        try {
            OutputStreamWriter osw = new OutputStreamWriter(out);
            write(osw, n, prettyPrint);
            osw.flush();
        } catch (IOException e) {
            throw new HatfieldException(e);
        }
    }


    public static void write(Writer out, Node n, boolean prettyPrint) {
        TransformerFactory tfactory = TransformerFactory.newInstance();
        Transformer serializer;

        try {
            serializer = tfactory.newTransformer();

            // Setup indenting to "pretty print"

            if (prettyPrint) {
                serializer.setOutputProperty(OutputKeys.INDENT, "yes");

                serializer.setOutputProperty(
                        "{http://xml.apache.org/xslt}indent-amount", "2");
            }
            serializer
                    .setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            serializer.transform(new DOMSource(n), new StreamResult(out));
        } catch (TransformerConfigurationException e) {
            throw new HatfieldException(e);
        } catch (TransformerException e) {
            throw new HatfieldException(e);
        }
    }


    public static void write(Writer out, Node n) {
        write(out, n, false);
    }


    public static void write(OutputStream out, Node n) {
        write(out, n, false);
    }


    public static Document newDocument() {
        return newDocumentNamespaceAware();
    }


    public static Document newDocumentNamespaceAware() {
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            return dbf.newDocumentBuilder().newDocument();
        } catch (ParserConfigurationException e) {
            throw new HatfieldException(e);
        }
    }


    public static void appendCopy(Node target, String insertionParent,
            Node source, String sourceSelector) {
        appendCopy(target, insertionParent, source, sourceSelector, null);
    }


    public static void appendCopy(Node target, String insertionParent,
            Node source, String sourceSelector, NamespaceContext ncr) {

        XPathUtil xpu = new XPathUtil();
        if (ncr != null) {
            xpu.setNamespaceContext(ncr);
        }
        Node n = xpu.evalNode(target, insertionParent);

        appendCopy(n, xpu.evalNodeList(source, sourceSelector));

    }


    public static void removeDuplicates(Node root, String selector,
            String indexExpression) {
        removeDuplicates(root, selector, indexExpression, null);
    }


    public static List<Node> findDuplicates(Node root, String nodeSelector,
            String indexExpression, NamespaceContext ncr) {
        XPathUtil xpu = new XPathUtil();
        if (ncr != null) {
            xpu.setNamespaceContext(ncr);
        }
        NodeList snl = xpu.evalNodeList(root, nodeSelector);
        HashSet<String> indexSet = new HashSet<String>();
        List<Node> toBeRemoved = new LinkedList<Node>();
        for (int i = 0; i < snl.getLength(); i++) {
            Node toBeMoved = snl.item(i);

            String index = xpu.evalString(toBeMoved, indexExpression);
            if (index != null && index.length() > 0) {
                if (indexSet.contains(index)) {
                    toBeRemoved.add(toBeMoved);
                } else {
                    indexSet.add(index);
                }
            }

        }
        return toBeRemoved;

    }


    public static void removeDuplicates(Node root, String selector,
            String indexExpression, NamespaceContext ncr) {
        List<Node> list = findDuplicates(root, selector, indexExpression, ncr);
        for (Node n : list) {
            Node parent = n.getParentNode();
            if (parent != null) {
                parent.removeChild(n);
            }

        }
    }


    public static Document getOwnerDocument(Node n) {
        if (n instanceof Document) {
            return (Document) n;
        } else {
            return n.getOwnerDocument();
        }
    }


    public static void appendCopy(Node destParent, Node src) {
        Node copy = getOwnerDocument(destParent).adoptNode(src.cloneNode(true));
        destParent.appendChild(copy);
    }


    public static void appendCopy(Node destParent, NodeList nl) {
        for (int i = 0; i < nl.getLength(); i++) {
            appendCopy(destParent, nl.item(i));
        }
    }
}
